package com.pinfly.common.crypto;

import java.io.IOException;
import java.math.BigInteger;

import com.pinfly.common.util.HexConvert;

/**
 * Static methods used to convert X.509 PKCS RSA keys and signatures into
 * Microsoft Crypto API format, and vice versa.
 */
public class KeyUtil
{
    private KeyUtil ()
    {
        // Can't instantiate this class
    }

    /**
     * Converts from a X.509 RSA Public key to a Microsoft RSA Public key
     * 
     * @param x509RSAKey The X.509 RSA Public key to convert in hex. It is
     *            assumed that the net.pinfly.util.HexConvert class was used to
     *            create the hex representation of this public key.
     * @return The Microsoft RSA Public key in hex
     * @throws IOException if the passed in hex key is not in proper format.
     */
    public static String convertFromX509ToMS (String x509RSAKey) throws IOException
    {
        return (convertFromX509ToMS (new X509RSAPublicKey (x509RSAKey)));

    }

    /**
     * Converts from a X.509 RSA Public key to a Microsoft RSA Public key
     * 
     * @param key The X.509 RSA Public key to convert
     * @return The Microsoft RSA Public key in hex
     */
    public static String convertFromX509ToMS (X509RSAPublicKey key)
    {
        MSRSAPublicKey mk = new MSRSAPublicKey (key);
        return (mk.getEncodedAsString ());
    }

    /**
     * Converts from a Microsoft RSA Public key to a X.509 RSA Public key
     * 
     * @param msKey The Microsoft RSA Public key to convert in hex. It is
     *            assumed that the net.pinfly.util.HexConvert class was used to
     *            create the hex representation of this public key.
     * @return The X.509 RSA Public key in hex
     * @throws IOException if the passed in hex key is not in proper format.
     */
    public static String convertFromMSToX509 (String msKey) throws IOException
    {
        return (convertFromMSToX509 (new MSRSAPublicKey (msKey)));

    }

    /**
     * Converts from a Microsoft RSA Public key to a X.509 RSA Public key
     * 
     * @param key The Microsoft RSA Public key to convert.
     * @return The X.509 RSA Public key in hex
     */
    public static String convertFromMSToX509 (MSRSAPublicKey key)
    {
        X509RSAPublicKey rsakey = new X509RSAPublicKey (key);
        return (rsakey.getEncodedAsString ());
    }

    /**
     * Converts a Microsoft CALG_RSA_SIGN signature into an MD5withRSA
     * signature. The converted signature, can be used by the JCE classes to
     * verify data signed by Microsoft's Crypto api.
     * 
     * @param calgRsaSig The Microsoft signature in hex. It is assumed that the
     *            net.pinfly.util.HexConvert class was used to create the hex
     *            representation of this signature.
     * @return The converted signature in hex.
     */
    public static String convertMSSignature (String calgRsaSig)
    {
        // The Microsoft CALG_RSA_SIGN signature is the same as
        // the MD5withRSA, that the Java JCE uses, except we
        // need to reverse the bytes.
        return (reverseBytes (calgRsaSig));
    }

    /**
     * Converts a MD5withRSA signature into a Microsoft CALG_RSA_SIGN signature.
     * The converted signature can be used by Microsoft's Crypto api to verify
     * data signed by the JCE classes.
     * 
     * @param md5WithRsaSig The JCE MD5withRSA signature in hex. It is assumed
     *            that the net.pinfly.util.HexConvert class was used to create
     *            the hex representation of this signature.
     * @return The converted signature in hex.
     */
    public static String convertSignatureToMS (String md5WithRsaSig)
    {
        // The MD5WithRSA is the same as Microsoft CALG_RSA_SIGN,
        // except we need to reverse the bytes.
        return (reverseBytes (md5WithRsaSig));
    }

    /**
     * Appends to a byte array
     * 
     * @param dataToAppend The data to append
     * @param src The array add to
     * @return The new byte array with the provided data appended
     */
    public static byte[] appendToArray (byte dataToAppend, byte[] src)
    {
        return (appendToArray (new byte[]
        { dataToAppend }, src));
    }

    /**
     * Appends to a byte array
     * 
     * @param dataToAppend The data to pre-pend
     * @param src The array add to
     * @return The new byte array with the provided data appended
     */
    public static byte[] appendToArray (byte[] dataToAppend, byte[] src)
    {
        return (addToArray (dataToAppend, src, false));
    }

    /**
     * Pre-pends to a byte array
     * 
     * @param dataToPrepend The data to pre-pend
     * @param src The array add to
     * @return The new byte array with the provided data pre-pended
     */
    public static byte[] prependToArray (byte dataToPrepend, byte[] src)
    {
        return (prependToArray (new byte[]
        { dataToPrepend }, src));
    }

    /**
     * Pre-pends to byte array
     * 
     * @param dataToPrepend The data to pre-pend
     * @param src The array add to
     * @return The new byte array with the provided data pre-pended
     */
    public static byte[] prependToArray (byte[] dataToPrepend, byte[] src)
    {
        return (addToArray (dataToPrepend, src, true));
    }

    /*
     * Pre-pends or appends to an array
     * 
     * @param dataToAdd The data to add
     * 
     * @param src The array add to
     * 
     * @param prepend True indicates to prepend, other append.
     * 
     * @return The new byte array with the provided data added
     */
    private static byte[] addToArray (byte[] dataToAdd, byte[] src, boolean prepend)
    {
        byte[] result = null;

        if (dataToAdd == null)
        {
            dataToAdd = new byte[0];
        }

        if (src == null)
        {
            src = new byte[0];
        }

        result = new byte[src.length + dataToAdd.length];

        if (prepend)
        {
            System.arraycopy (dataToAdd, 0, result, 0, dataToAdd.length);
            System.arraycopy (src, 0, result, dataToAdd.length, src.length);
        }
        else
        {
            // Append
            System.arraycopy (src, 0, result, 0, src.length);
            System.arraycopy (dataToAdd, 0, result, src.length, dataToAdd.length);
        }

        return (result);
    }

    /**
     * Reverses bytes in a hex string.
     * 
     * @param input The hex string to reverse. It assumes that this hex string
     *            was created using the net.pinfly.util.HexConvert class.
     * @return The input string with the bytes reversed.
     */
    public static String reverseBytes (String input)
    {
        if (input == null)
        {
            return (null);

        }

        byte[] result = HexConvert.convertToBytes (input);
        reverseBytes (result);
        return (HexConvert.convertHexString (result));
    }

    /**
     * Reverses the bytes in the input array.
     * 
     * @param input A byte array that contains the bytes that we wish to
     *            reverse. After this method, the bytes in the array will have
     *            been reversed.
     * @throws IllegalArgumentException if input is null
     */
    public static void reverseBytes (byte[] input)
    {
        reverseBytes (input, 0, input.length);
    }

    /**
     * Reverses the bytes in the range given. Usefull for converting between
     * BigEndian and LittleEndian.
     * 
     * @param input A byte array that contains the bytes that we wish to
     *            reverse. After this method, the bytes in the array will have
     *            been reversed.
     * @param offset The offset into the array to start the reverse. A zero
     *            indicates to start at the first element of the input array.
     * @param count The number of bytes to reverse.
     * @throws IllegalArgumentException if input is null, offset is greater than
     *             length, or offset plus count is greater than length.
     */
    public static void reverseBytes (byte[] input, int offset, int count)
    {
        if (input == null || input.length == 0)
        {
            throw new IllegalArgumentException ("input array must have contents!");
        }

        if (offset < 0)
        {
            throw new IllegalArgumentException ("offset must be positive or zero!");
        }

        if (count < 0)
        {
            throw new IllegalArgumentException ("count must be positive or zero!");
        }

        if (offset + count > input.length)
        {
            throw new IllegalArgumentException ("offset plus length must be less than the size of the input array!");
        }

        byte[] tmpArray = new byte[count];

        for (int i = offset + count - 1, j = 0; i >= offset; i--, j++)
        {
            tmpArray[j] = input[i];
        }

        // Replace the values
        System.arraycopy (tmpArray, 0, input, offset, count);
    }

    /**
     * Converts a BigInteger into bytes. The output is the same as
     * BigInteger.toByteArray except that if there is a positive sign byte, it
     * is removed.
     * 
     * @param i The BigInteger to obtain the 2's complement byte array
     *            representation.
     * @return A new byte array that has the positive sign byte removed. If
     *         there is no positive sign byte, the results of
     *         BigInteger.toByteArray is returned.
     */
    public static byte[] convertToBytes (BigInteger i)
    {
        if (i == null)
        {
            return (new byte[0]);
        }

        byte[] iBytes = i.toByteArray ();

        byte[] result = null;

        if (iBytes[0] == 0)
        {
            result = new byte[iBytes.length - 1];
            System.arraycopy (iBytes, 1, result, 0, iBytes.length - 1);
        }
        else
        {
            result = iBytes;
        }

        return (result);

    }

}